﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using Taste.Internal;
using System.Globalization;

namespace Taste
{
    /// <summary>
    /// The state machine implementation.
    /// </summary>
    /// <typeparam name="TState">An enum representing the states of the machine.</typeparam>
    /// <typeparam name="TTrigger">An enum representing the triggers handled by the states.</typeparam>
    public class StateMachine<TState, TTrigger> : IStateMachine<TState, TTrigger>
        where TState : struct
        where TTrigger : struct
    {
        private Dictionary<TState, StateDefinition<TState, TTrigger>> _stateDefs;
        private Action<IStateError<TState>> _defaultErrorHandler;
        private Dictionary<TTrigger, Trigger<TState, TTrigger>> _triggers;
        private TriggerEffectCollection<TTrigger> _defaultTriggerEffects;
        private ICurrentStateImpl<TState, TTrigger> _currentStateImpl;

        /// <summary>
        /// Constructor.
        /// </summary>
        public StateMachine()
        {
            _stateDefs = new Dictionary<TState, StateDefinition<TState, TTrigger>>();
            _triggers = new Dictionary<TTrigger, Trigger<TState, TTrigger>>();
            _currentStateImpl = new InitialState<TState, TTrigger>();
            _defaultTriggerEffects = new TriggerEffectCollection<TTrigger>();
            _defaultErrorHandler = null;
        }

        #region IStateMachine members
        public void DefineState(TState name, Action<ICurrentState<TState, TTrigger>> onEntered, Action<IStateTransition<TState>> onLeaving)
        {
            if(onEntered == null)
                throw new ArgumentNullException("onEntered");

            if(onLeaving == null)
                throw new ArgumentNullException("onLeaving");

            _stateDefs[name] = new StateDefinition<TState, TTrigger>()
            {
                Name = name,
                OnEntered = onEntered,
                OnLeaving = onLeaving
            };
        }

        public void OnFiredDefault(TTrigger triggerName, Action effect, Func<bool> condition = null)
        {
            _defaultTriggerEffects.SetEffect(triggerName, effect, condition);
            this.TriggerEffectChanged(triggerName);
        }

        public void OnFiredDefault<TParameter>(TTrigger triggerName, Action<TParameter> effect, Func<TParameter, bool> condition = null)
        {
            _defaultTriggerEffects.SetEffect(triggerName, effect, condition);
            this.TriggerEffectChanged(triggerName);
        }

        public void OnErrorDefault(Action<IStateError<TState>> action)
        {
            _defaultErrorHandler = action;
        }

        public ITrigger GetTrigger(TTrigger triggerName)
        {
            return this.GetTriggerImpl(triggerName);
        }

        public void Start(TState initialState, object userData = null)
        {
            _currentStateImpl = null;
            this.EnterState(initialState, userData);
        }

        public ICurrentState<TState, TTrigger> CurrentState { get { return this.CurrentStateImpl; } }

        public event PropertyChangedEventHandler PropertyChanged;
        public event EventHandler CurrentStateChanged;
        #endregion

        internal bool CanFireTrigger(TTrigger triggerName, object parameter)
        {
            return this.CurrentStateImpl.CanFire(triggerName, parameter) 
                ?? _defaultTriggerEffects.CanFire(triggerName, parameter) 
                ?? false;
        }

        internal void FireTrigger(TTrigger triggerName, object parameter)
        {
            try
            {
                if(!this.CurrentStateImpl.Fire(triggerName, parameter))
                    _defaultTriggerEffects.Fire(triggerName, parameter);
            }
            catch(Exception ex)
            {
                this.ErrorHappened(this.CurrentStateImpl.Name, ex);
            }
        }

        internal void TriggerEffectChanged(TTrigger triggerName)
        {
            Trigger<TState, TTrigger> trigger;
            if(_triggers.TryGetValue(triggerName, out trigger))
                trigger.RaiseCanFireChanged();
        }

        internal void ErrorHappened(TState stateName, Exception error)
        {
            if(!this.CurrentState.Name.Equals(stateName))
                return;

            StateError<TState> stateError = new StateError<TState>(error);

            this.CurrentStateImpl.ErrorHappened(stateError);
            if(!stateError.IsHandled && _defaultErrorHandler != null)
                _defaultErrorHandler(stateError);

            if(!stateError.IsHandled)
                throw error;

            this.CurrentStateImpl.GoingToNextState();

            this.EnterState(stateError.NextStateName, stateError.NextUserData);
        }

        private Trigger<TState,TTrigger> GetTriggerImpl(TTrigger triggerName)
        {
            Trigger<TState, TTrigger> trigger;
            if(!_triggers.TryGetValue(triggerName, out trigger))
            {
                trigger = new Trigger<TState, TTrigger>(triggerName, this);
                _triggers.Add(triggerName, trigger);
            }
            return trigger;
        }

        private void EnterState(TState stateName, object userData)
        {
            TState? previousStateName = (this.CurrentState == null ? null : new TState?(this.CurrentState.Name));

            StateDefinition<TState, TTrigger> currentStateDef;
            if(!_stateDefs.TryGetValue(stateName, out currentStateDef))
                throw new StateException(string.Format("Cannot enter state '{0}' : no definition found.", stateName, CultureInfo.CurrentCulture), null);

            this.CurrentStateImpl = new CurrentState<TState, TTrigger>(this, currentStateDef, previousStateName, userData);
            foreach(var trigger in _triggers.Values)
                trigger.RaiseCanFireChanged();
            try
            {
                this.CurrentStateImpl.Entered();
            }
            catch(Exception error)
            {
                this.ErrorHappened(stateName, error);
            }
        }

        internal bool LeaveState(TState stateName, object userData)
        {
            if(!this.CurrentState.Name.Equals(stateName))
                return false;

            StateTransition<TState> stateTransition = new StateTransition<TState>(userData);
            this.CurrentStateImpl.Leaving(stateTransition);

            if(!stateTransition.IsAccepted.HasValue)
                throw new StateException("State transition must be accepted or rejected.", null);

            if(!stateTransition.IsAccepted.Value)
                return false;

            this.CurrentStateImpl.GoingToNextState();

            this.EnterState(stateTransition.NextStateName, stateTransition.NextUserData);
            return true;
        }

        private ICurrentStateImpl<TState, TTrigger> CurrentStateImpl
        {
            get { return _currentStateImpl; }
            set
            {
                if(_currentStateImpl != value)
                {
                    _currentStateImpl = value;
                    if(this.CurrentStateChanged != null)
                        this.CurrentStateChanged(this, EventArgs.Empty);
                    if(this.PropertyChanged != null)
                        this.PropertyChanged(this, new PropertyChangedEventArgs("CurrentState"));
                }
            }
        }
    }
}
